"use strict";

var boxPadding = 3;
var fontHeight = 16;
var menuWidthPadding = 2;
var menuHeightPadding = 1;
var handleWidth = 40;
var topTextIndent = 28;
var containerIndentWidth = 20;
var containerTabHeight = 14;
var emptyNestedBlockHeight = 20;
var boxFont = fontHeight + "px Calibri";

var factoryAreaWidth = 180;
var mainBlockOffsetX = 20;
var mainBlockOffsetY = 20;

var menuPopupItemPadding = 1;
var menuBackgroundColor = "eee";
var menuTextColor = "000";
var menuBackgroundActiveColor = "aaa";
var menuTextActiveColor = "000";

var distanceThreshold2 = 15 * 15;

var ConnectorTypes = {
    statement : {}
};

var EditorState = {
  None : {},              // normal state when nothing is happening
  Dragging : {},          // dragging a block
  MenuButtonClicked : {}, // clicked a menu button (so if the button is released, the menu pop-up stays up)
  MenuPreChoice : {},     // clicked and released a menu button (so the popup stays up)
  MenuChoosing : {},      // making a choice inside a popup
};

function assert(b) {
    if (!b) throw "Assertion failed";
}

// Draw the connector bit (TODO - clean this up, give it better name and
// parametrize on the connector size).
function handlePath(ctx, x, y, f) {
    ctx.bezierCurveTo(x + f * 20, y + 0, x + f * 20, y + 0, x + f * 15, y + 5);
    ctx.bezierCurveTo(x + f * 10, y + 10, x + f * 30, y + 10, x + f * 25, y + 5);
    ctx.bezierCurveTo(x + f * 20, y + 0, x + f * 20, y + 0, x + f * 40, y + 0);
}

function alwaysTrue() { return true; } 
function alwaysFalse() { return false; } 

// Blocks must have the following methods:
// .pointIn(x, y) - determines whether a point is inside the block
// .draw(ctx) draws this connector on the given context
// 
// Fields:
// .x, .y - coordinates
// .childrenSlots - returns the array [{x, y, type, block}] of connectors from the block
// .parentSlot - returns the connector {x, y, type, block} to the parent

// Build procedure start block
function buildProcStart(auxCtx, text, onDelete, userData) {
    var item = {};

    auxCtx.font = boxFont;
    var metrics = auxCtx.measureText(text);
    var w = Math.max(handleWidth, 2 * boxPadding + metrics.width);
    var h = fontHeight + 2 * boxPadding;
    var textHeight = fontHeight;

    item.x = 0;
    item.y = 0;
    item.userData = userData;
    item.onDelete = onDelete;

    item.updateAndGetBounds = function(children) {
        var restBounds = (children[0] === null) ? { width : 0, height : 0} : children[0];
        return { width : Math.max(w, restBounds.width) , height : h + restBounds.height };
    };

    item.pointIn = function (pointX, pointY, blockX, blockY) {
        return pointX >= blockX && 
            pointY >= blockY && 
            pointX < blockX + w &&
            pointY < blockY + h;
    };

    item.childrenSlots = [{ 
        offsetX : 20, 
        offsetY : h + 7,
        connectorType : ConnectorTypes.statement,
        isInput : false,
        block : null
    }];

    item.parentSlot = null;

    item.menus = [];

    item.draw = function(ctx, dx, dy) {
        ctx.lineWidth = 1;
        ctx.strokeStyle = "6c6";
        ctx.fillStyle = "393";

        ctx.beginPath();
        ctx.setTransform(1, 0, 0, 1, dx, dy);
        ctx.moveTo(0, 0);
        ctx.lineTo(w, 0);
        ctx.lineTo(w, h);
        ctx.lineTo(handleWidth, h);
        handlePath(ctx, handleWidth, h, -1);
        ctx.lineTo(0, 0);
        ctx.fill();
        ctx.stroke();

        ctx.fillStyle = "fff";
        ctx.textAlign = "left";
        ctx.textBaseline = "middle";
        ctx.font = boxFont;
        ctx.fillText(text, boxPadding, boxPadding + textHeight / 2);
    }

    return item;
}

// Calculate metrics for a text block
function calculateMenuItemsWidth(ctx, items) {
    var width = 0;
    for (var i = 0; i < items.length; i++) {
        var metrics = ctx.measureText(items[i]);
        if (metrics.width > width) {
            width = metrics.width;
        }
    }
    return width;
}

function createMenuFromOptions(ctx, offsetX, centerY, options) {
    // Calculate the size of the menu stuff
    var width = calculateMenuItemsWidth(ctx, options);
    var menuFontHeight = fontHeight;
    var menuItemHeight = fontHeight + 2 * menuPopupItemPadding;

    return {
        width : (options.length === 0) ? 0 : width + 2 * menuWidthPadding,
        height : (options.length === 0) ? 0 : menuFontHeight + 2 * menuHeightPadding,
        fontHeight : menuFontHeight,
        offsetX : offsetX,
        offsetY : centerY - fontHeight / 2 - menuHeightPadding,
        centerTextY : centerY,
        options : options,
        isMenu : true,
        currentOption : 0
    };
}

function getMenuPopupMeasures(menu, baseX, baseY) {
    return { 
        x : baseX + menu.offsetX,
        y : baseY + menu.offsetY + menu.height,
        itemHeight : 2 * menuPopupItemPadding + menu.fontHeight
    }; 
}

function drawMenu(ctx, menu, baseX, baseY) {
    var menuMeasures = getMenuPopupMeasures(menu, baseX, baseY);
    var x = menuMeasures.x;
    var y = menuMeasures.y;

    var width = menu.width;
    var itemHeight = menuMeasures.itemHeight; 
 
    for (var i = 0; i < menu.options.length; i++) {
        ctx.fillStyle = (i === menu.currentOption) ? menuBackgroundColor : menuBackgroundActiveColor;
        ctx.fillRect(x, y + i * itemHeight, width, itemHeight); 

        ctx.font = boxFont;
        ctx.fillStyle = (i === menu.currentOption) ? menuTextColor : menuTextActiveColor;
        ctx.textAlign = "center";
        ctx.textBaseline = "top";
        ctx.fillText(menu.options[i], x + menu.width / 2, y + i * itemHeight + menuPopupItemPadding);
    }
}

function pointToOption(x, y, menu, baseX, baseY) {
    var menuMeasures = getMenuPopupMeasures(menu, baseX, baseY);
    x -= menuMeasures.x;
    y -= menuMeasures.y;

    // Check that the point is within the menu horizontally
    if (x < 0 || x >= menu.width) {
        return -1;
    }

    y = Math.floor(y / menuMeasures.itemHeight);
    if (y < 0 || y >= menu.options.length) {
        return -1;
    }

    return y;
}

// Create a loop block
function buildLoopBlock(auxCtx, text, options, userData) {
    var item = {};

    // Get the size of the text
    auxCtx.font = fontHeight + "px Calibri";
    var metrics = auxCtx.measureText(text);
    var textHeight = fontHeight;

    // Create menu
    var centerTextY = boxPadding + textHeight / 2;
    var textEndOffset = metrics.width + 2 * boxPadding + topTextIndent;
    var menu = createMenuFromOptions(auxCtx, textEndOffset, centerTextY, options);

    // The width must be able to contain the text and the bottom handle
    var topBitWidth = textEndOffset + menu.width + boxPadding;
    var w = Math.max(topBitWidth, handleWidth + containerIndentWidth);

    // Calculate height of the top bit
    var topHeight = fontHeight + 2 * boxPadding;
    var h = fontHeight + 2 * boxPadding;

    var bbheight = emptyNestedBlockHeight; // TODO

    item.x = 0;
    item.y = 0;
    item.userData = userData;
    item.onDelete = alwaysTrue;

    item.menus = [ menu ];

    item.clone = function() {
        var c = buildLoopBlock(auxCtx, text, options, userData);
        c.x = item.x;
        c.y = item.y;
        return c;
    };

    item.updateAndGetBounds = function(children) {
        var bodyBounds = (children[1] === null) ? 
          { width : 10, height : emptyNestedBlockHeight} : children[1];

        bbheight = bodyBounds.height;
        item.childrenSlots[0].offsetY = h + containerTabHeight + bbheight + 7;

        var restBounds = (children[0] === null) ? { width : 0, height : 0} : children[0];

        var bbwidth = Math.max(Math.max(w, restBounds.width), bodyBounds.width + containerIndentWidth); 

        return { width : bbwidth, height : h + containerTabHeight + bbheight + restBounds.height };
    };

    item.pointIn = function (pointX, pointY, blockX, blockY) {
        return pointX >= blockX && 
            pointY >= blockY && 
            pointX < blockX + w &&
            pointY < blockY + h;
    };

    item.childrenSlots = [
        { 
            offsetX : 20, 
            offsetY : h + containerTabHeight + bbheight + 7,
            connectorType : ConnectorTypes.statement,
            isInput : false,
            block : null 
        },
        { 
            offsetX : 20 + containerIndentWidth, 
            offsetY : h + 7,
            connectorType : ConnectorTypes.statement,
            isInput : false,
            block : null 
        }
    ];

    item.parentSlot = { 
        offsetX : 20, 
        offsetY : 7,
        connectorType : ConnectorTypes.statement,
        isInput : true,
        block : null
    };

    item.draw = function(ctx, dx, dy) {
        ctx.lineWidth = 1;
        ctx.strokeStyle = "b9b";
        ctx.fillStyle = "b3b";

        ctx.beginPath();
        ctx.setTransform(1, 0, 0, 1, dx, dy);
        ctx.moveTo(0, 0);
        handlePath(ctx, 0, 0, 1);
        ctx.lineTo(w, 0);
        ctx.lineTo(w, h);

        ctx.lineTo(handleWidth + containerIndentWidth, h);
        handlePath(ctx, handleWidth + containerIndentWidth, h, -1);

        ctx.lineTo(containerIndentWidth, h + bbheight);

        handlePath(ctx, containerIndentWidth, h + bbheight, 1);

        ctx.lineTo(containerIndentWidth + handleWidth, h + bbheight + containerTabHeight);

        handlePath(ctx, handleWidth, h + bbheight + containerTabHeight, -1);
        ctx.lineTo(0, 0);

        ctx.fill();
        ctx.stroke();

        // Draw the text
        ctx.font = boxFont;
        ctx.fillStyle = "fff";
        ctx.textAlign = "left";
        ctx.textBaseline = "middle";
        ctx.fillText(text, topTextIndent + boxPadding, boxPadding + textHeight / 2);

        // Draw the menu item
        if (options.length > 0) {
            ctx.fillStyle = "99f";
            ctx.fillRect(menu.offsetX, menu.offsetY, menu.width, menu.height);
            ctx.fillStyle = "fff";
            ctx.textAlign = "center";
            ctx.fillText(options[menu.currentOption], menu.offsetX + menu.width / 2, boxPadding + textHeight / 2);
        }
    }

    return item;
}

// Create a statement block
function buildSimpleBlock(auxCtx, text, userData, onDelete) {
    var item = {};

    auxCtx.font = fontHeight + "px Calibri";
    var metrics = auxCtx.measureText(text);
    var textHeight = fontHeight;
    var w = metrics.width + 2 * boxPadding + topTextIndent;
    var h = fontHeight + 2 * boxPadding;

    item.x = 0;
    item.y = 0;
    item.userData = userData;
    item.onDelete = function() { return true; }

    item.updateAndGetBounds = function(children) {
        var restBounds = (children[0] === null) ? { width : 0, height : 0} : children[0];
        return { width : Math.max(w, restBounds.width) , height : h + restBounds.height };
    };

    item.clone = function() {
        var c = buildSimpleBlock(auxCtx, text, userData);
        c.x = item.x;
        c.y = item.y;
        return c;
    };

    item.pointIn = function (pointX, pointY, blockX, blockY) {
        return pointX >= blockX && 
            pointY >= blockY && 
            pointX < blockX + w &&
            pointY < blockY + h;
    };

    item.childrenSlots = [
        { 
            offsetX : 20, 
            offsetY : h + 7,
            connectorType : ConnectorTypes.statement,
            isInput : false,
            block : null 
        }
    ];

    item.menus = [];

    item.parentSlot = { 
        offsetX : 20, 
        offsetY : 7,
        connectorType : ConnectorTypes.statement,
        isInput : true,
        block : null
    };

    item.draw = function(ctx, dx, dy) {
        ctx.font = boxFont;
        ctx.lineWidth = 1;
        ctx.strokeStyle = "aaf";
        ctx.fillStyle = "33f";

        ctx.beginPath();
        ctx.setTransform(1, 0, 0, 1, dx, dy);
        ctx.moveTo(0, 0);
        handlePath(ctx, 0, 0, 1);
        ctx.lineTo(w, 0);
        ctx.lineTo(w, h);
        ctx.lineTo(handleWidth, h);
        handlePath(ctx, handleWidth, h, -1);
        ctx.lineTo(0, 0);
        ctx.fill();
        ctx.stroke();

        ctx.fillStyle = "fff";
        ctx.textAlign = "left";
        ctx.textBaseline = "middle";
        ctx.fillText(text, topTextIndent + boxPadding, boxPadding + textHeight / 2);
    }

    return item;
}

function childFromBlockAndPoint(current, pointX, pointY, blockX, blockY) {
    if (current.pointIn(pointX, pointY, blockX, blockY)) {
        current.x = blockX;
        current.y = blockY;

        for (var i = 0; i < current.menus.length; i++) {
            var m = current.menus[i];
            if (pointX >= blockX + m.offsetX && pointX < blockX + m.offsetX + m.width &&
                pointY >= blockY + m.offsetY && pointY < blockY + m.offsetY + m.height) {
                return { block : current, menu : m };
            }
        }

        return { block : current, menu : null };
    }

    var children = current.childrenSlots;
    if (children == null) {
        return null;
    }

    var res = null;
    for (var i = 0; i < children.length; i++) {
        var childBlock = children[i].block;
        if (childBlock != null) {
            var nx = blockX + children[i].offsetX - childBlock.parentSlot.offsetX;
            var ny = blockY + children[i].offsetY - childBlock.parentSlot.offsetY;
            var res = childFromBlockAndPoint(childBlock, pointX, pointY, nx, ny);
            if (res != null) {
                break;
            }
        }
    }

    return res;
}

function childFromBlockListAndPoint(list, x, y) {
    var res = null;
    for (var i = list.length - 1; i >= 0; i--) {
        if (list[i] != null) {
            var res = childFromBlockAndPoint(list[i], x, y, list[i].x, list[i].y);
            if (res != null) {
                break;
            }
        }
    }
    return res;
}

function removeChildBlock(parent, childToBeRemoved) {
    assert(parent.childrenSlots != null);
    var slots = parent.childrenSlots;
    for (var i = 0; i < slots.length; i++) {
        if (slots[i] !== null && slots[i].block === childToBeRemoved) {
            slots[i].block = null;
            assert(childToBeRemoved.parentSlot.block === parent);
            childToBeRemoved.parentSlot.block = null;
            return;
        }
    }
    
    throw "Could not find the child.";
}

function addToTopLevelBlocks(topLevelList, block) {
    if (block.parentSlot !== null) {
        block.parentSlot.block = null;
    }
    topLevelList.push(block);
}

// Recalculates sizes of container blocks from its children
function recalcBlockSizesList(topLevelBlocks) {
    for (var i = 0; i < topLevelBlocks.length; i++) {
        recalcBlockSizes(topLevelBlocks[i]); // Ignore the returned bounding box for now
                                             // TODO - use the bounding box to resize the
                                             //        canvas?
    }
}

function recalcBlockSizes(block) {
    var children = [];
    for (var i = 0; i < block.childrenSlots.length; i++) {
        if (block.childrenSlots[i].block !== null) {
            children.push(recalcBlockSizes(block.childrenSlots[i].block));
        }
        else {
            children.push(null);
        }
    }
    return block.updateAndGetBounds(children);
}

function areMatchingConnectors(conA, conB) {
    return conA.connectorType === conB.connectorType && conA.isInput !== conB.isInput;
}

// Finds the closest compatible connection between the parent pin of 
// the inputBlock and some child connector of outputBlockHierarchy.
// bestSoFar is the best connector combination we have found so far.
function bestDistanceConnector(inputBlock, outputBlockHierarchy, outX, outY, bestSoFar, considerInnerPins) {
    if (inputBlock.parentSlot === null) {
        return bestSoFar;
    }

    var parentX = inputBlock.x + inputBlock.parentSlot.offsetX;
    var parentY = inputBlock.y + inputBlock.parentSlot.offsetY;

    for (var i = 0; i < outputBlockHierarchy.childrenSlots.length; i++) {
        var childSlot = outputBlockHierarchy.childrenSlots[i];

        var childX = outX + childSlot.offsetX;
        var childY = outY + childSlot.offsetY;

        if ((considerInnerPins || childSlot.block === null) &&
            areMatchingConnectors(inputBlock.parentSlot, childSlot)) {
            var dx = childX - parentX;
            var dy = childY - parentY;
            var d2 = dx * dx + dy * dy;
            if (bestSoFar === null || d2 < bestSoFar.d2) {
                bestSoFar = { 
                    d2 : d2, 
                    dx : dx,
                    dy : dy,
                    outputSlot : childSlot, 
                    outputBlock : outputBlockHierarchy, 
                    inputBlock : inputBlock
                };
            }
        }

        if (childSlot.block !== null) {
            var nx = outX + childSlot.offsetX - childSlot.block.parentSlot.offsetX;
            var ny = outY + childSlot.offsetY - childSlot.block.parentSlot.offsetY;
            bestSoFar = bestDistanceConnector(inputBlock, childSlot.block, nx, ny, bestSoFar, considerInnerPins);
        }
    }

    return bestSoFar;
}

function drawBlockHierarchy(ctx, block, x, y) {
    block.draw(ctx, x, y);
    if (block.childrenSlots !== null) {
        for (var i = 0; i < block.childrenSlots.length; i++) {
            var slot = block.childrenSlots[i];
            if (slot.block !== null) {
                var nx = x + slot.offsetX - slot.block.parentSlot.offsetX;
                var ny = y + slot.offsetY - slot.block.parentSlot.offsetY;
                drawBlockHierarchy(ctx, slot.block, nx, ny);
            }
        }
    }
    ctx.setTransform(1, 0, 0, 1, 0, 0);
}

function extractUserDataTree(b) {
    var children = [];
    for (var i = 0; i < b.childrenSlots.length; i++) {
        if (b.childrenSlots[i].block != null) {
            children.push(extractUserDataTree(b.childrenSlots[i].block));
        }
        else {
            children.push(null);
        }
    }
    var node = {
        userData : b.userData,
        children : children
    };
    return node;
}

function recalcFactoryLayout(factoryBlocks) {
    // Factory blocks
    var factoryY = 10;
    var factoryX = 10;
    var factoryPadding = 12;
    for (var i = 0; i < factoryBlocks.length; i++) {
        var block = factoryBlocks[i];
        block.x = factoryX;
        block.y = factoryY;
        factoryY += recalcBlockSizes(block).height + factoryPadding; 
    }
}

function removeChildFromSlot(slot) {
    if (slot.block.childrenSlots.length === 1 &&
        slot.block.childrenSlots[0].block !== null && 
        areMatchingConnectors(slot, slot.block.childrenSlots[0].block.parentSlot)) {
        slot.block = slot.block.childrenSlots[0].block;
        return;
    }
    for (var i = 0; i < slot.block.childrenSlots.length; i++) {
        var grandChild = slot.block.childrenSlots[i].block;
        // TODO add to toplevel
    }
    slot.block = null;
}

function removeBlocksByUserData(block, userDataFilter) {
    var dirty = false;
    for (var i = 0; i < block.childrenSlots.length; i++) {
        var child = block.childrenSlots[i].block;
        if (child !== null) {
            if (removeBlocksByUserData(child, userDataFilter)) {
                dirty = true;
            }
            if (userDataFilter(child.userData)) {
                removeChildFromSlot(block.childrenSlots[i]);
                dirty = true;
            }
        }
    }
    return dirty;
}

function removeBlocksByUserDataFromList(blockList, userDataFilter) {
    for (var i = 0; i < blockList.length; i++) {
        var block = blockList[i];
        if (block.userData && userDataFilter(block.userData)) {
            // Remove the block from the list
            blockList.splice(i, 1);
            i--;
        }
        else {
            if (removeBlocksByUserData(block, userDataFilter)) {
                recalcBlockSizes(block);
            }
        }
    }
}

function main() {
    var topLevelBlocks = [];
    var factoryBlocks = [];

    var $runButton = document.getElementById("runButton");

    $runButton.onclick = function() { 
        for (var i = 0; i < topLevelBlocks.length; i++) {
            var b = topLevelBlocks[i];
            console.log(extractUserDataTree(topLevelBlocks[i]));   
        }
    };

    var $dbg = document.getElementById('dbg');
    $dbg.innerText = "abc\n123\nasjaklk";
    var $editCanvas = document.getElementById('editor');
    var dirty = true;

    var blockMoveState = {
        x : -1,
        y : -1,
        block : null,
        menu : null,
        state : EditorState.None
    }; 

    var tempCtx = $editCanvas.getContext('2d');
    (function() {
        // Factory blocks
        var addFactory = function(block) { factoryBlocks.push(block); };


        function removeCall(procId) {
            return function() {
                function callFilter(userData) {
                    return userData && 
                        userData.kind === "CALL" &&
                        userData.id === procId;
                }
                removeBlocksByUserDataFromList(topLevelBlocks, callFilter);

                for (var i = 0; i < factoryBlocks.length; i++) {
                    var block = factoryBlocks[i];
                    console.log(block.userData);
                    if (block.userData && 
                        block.userData.kind === "CALL" &&
                        block.userData.id === procId) {
                        factoryBlocks.splice(i, 1);
                        recalcFactoryLayout(factoryBlocks);
                        return true;
                    }
                }

                return true;
            };
        }

        addFactory(buildLoopBlock(tempCtx, "While", ["free", "wall", "not free", "mark"], { kind : "WHILE" }));
        
        var procFactoryBlock = buildProcStart(tempCtx, "Procedure", alwaysTrue);
        var procCounter = 0;
        procFactoryBlock.clone = function() {
            procCounter++;
            var procId = procCounter + "";
            var c = buildProcStart(tempCtx, "Procedure " + procCounter, removeCall(procId), { kind : "PROC", id : procId });
            c.x = procFactoryBlock.x;
            c.y = procFactoryBlock.y;

            // Add factory guy for calls
            var callStmt = buildSimpleBlock(tempCtx, "Call " + procId, { kind: "CALL", id : procId });
            factoryBlocks.push(callStmt);
            recalcFactoryLayout(factoryBlocks);

            return c;
        };
        addFactory(procFactoryBlock);

        addFactory(buildSimpleBlock(tempCtx, "Step", { kind: "STEP" }));
        addFactory(buildSimpleBlock(tempCtx, "Turn left", { kind : "TURN_LEFT" }));
        addFactory(buildSimpleBlock(tempCtx, "Put brick", { kind : "PUT_BRICK" }));
        addFactory(buildSimpleBlock(tempCtx, "Take brick", { kind : "TAKE_BRICK" }));
        addFactory(buildSimpleBlock(tempCtx, "Put mark", { kind : "PUT_MARK" }));
        addFactory(buildSimpleBlock(tempCtx, "Take mark", { kind : "TAKE_MARK" }));

        recalcFactoryLayout(factoryBlocks);

        // Main block
        var mainBlock = buildProcStart(tempCtx, ">> Main program <<", alwaysFalse, { kind : "MAIN" });
        mainBlock.x = factoryAreaWidth + mainBlockOffsetX;
        mainBlock.y = mainBlockOffsetY;
        addToTopLevelBlocks(topLevelBlocks, mainBlock);
    })();

    var redraw = function() {
        if (dirty) {
            var ctx = $editCanvas.getContext('2d');
            ctx.setTransform(1, 0, 0, 1, 0, 0);
            ctx.clearRect(0, 0, $editCanvas.width, $editCanvas.height); 

            ctx.fillStyle = "ccc";
            ctx.fillRect(0, 0, factoryAreaWidth, $editCanvas.height);

            for (var i = 0; i < factoryBlocks.length; i++) {
                drawBlockHierarchy(ctx, factoryBlocks[i], factoryBlocks[i].x, factoryBlocks[i].y);
            }

            for (var i = 0; i < topLevelBlocks.length; i++) {
                drawBlockHierarchy(ctx, topLevelBlocks[i], topLevelBlocks[i].x, topLevelBlocks[i].y);
            }

            if (blockMoveState.state === EditorState.Dragging) {
                drawBlockHierarchy(ctx, blockMoveState.block, blockMoveState.block.x, blockMoveState.block.y);
            }
            else if (blockMoveState.state === EditorState.MenuButtonClicked ||
                blockMoveState.state === EditorState.MenuPreChoice ||
                blockMoveState.state === EditorState.MenuChoosing) {
                    drawMenu(ctx, blockMoveState.menu, blockMoveState.block.x, blockMoveState.block.y);
            }

            dirty = false;
        }
    }

    var callMouseEvent = function(f) { return function(e) {
        var rect = $editCanvas.getBoundingClientRect();
        f(e.clientX - rect.left, e.clientY - rect.top);
    };};

    var callTouchEvent = function(f) { return function(e) {
        e.preventDefault();
        var rect = $editCanvas.getBoundingClientRect();
        f(e.touches[0].clientX - rect.left, e.touches[0].clientY - rect.top);
    };};

    var callTouchEndEvent = function(f) { return function(e) {
        var rect = $editCanvas.getBoundingClientRect();
        f(e.changedTouches[0].clientX - rect.left, e.changedTouches[0].clientY - rect.top);
    };};

    var onDragStart = function(x, y) {
        var childToBeMoved = childFromBlockListAndPoint(factoryBlocks, x, y);
        if (childToBeMoved === null) {
            childToBeMoved = childFromBlockListAndPoint(topLevelBlocks, x, y);
        }
        else {
            childToBeMoved.menu = null; // Ignore menu clicks for factories
            childToBeMoved.block = childToBeMoved.block.clone();
            addToTopLevelBlocks(topLevelBlocks, childToBeMoved.block);
        }
        if (blockMoveState.state === EditorState.MenuPreChoice) {
            // Last click activated a menu -> if we clicked inside the menu popup,
            // start the menu choice mode. Otherwise fall back to dragging...
            var option = pointToOption(x, y, blockMoveState.menu, blockMoveState.block.x, blockMoveState.block.y);
            if (option != -1) {
                if (blockMoveState.menu.currentOption !== option) {
                    blockMoveState.menu.currentOption = option;
                    dirty = true;
                }
                blockMoveState.state = EditorState.MenuChoosing;
                return;
            }
        }

        if (childToBeMoved != null) {
            var blockToBeMoved = childToBeMoved.block;
            blockMoveState.x = x - blockToBeMoved.x;
            blockMoveState.y = y - blockToBeMoved.y;
            blockMoveState.xOriginal = blockToBeMoved.x;
            blockMoveState.yOriginal = blockToBeMoved.y;
            blockMoveState.block = blockToBeMoved;
            if (childToBeMoved.menu !== null) {
                // If the block is menu item, start the choice
                blockMoveState.menu = childToBeMoved.menu;
                blockMoveState.state = EditorState.MenuButtonClicked;
                dirty = true;
            }
            else {
                blockMoveState.state = EditorState.Dragging;
                // If the block is connected to a parent -> disconnect it
                if (blockToBeMoved.parentSlot !== null && 
                    blockToBeMoved.parentSlot.block !== null) {
                    removeChildBlock(blockToBeMoved.parentSlot.block, blockToBeMoved);
                    recalcBlockSizesList(topLevelBlocks);
                }
                else {
                    // Remove from the top level list
                    var idxToBeMoved = topLevelBlocks.indexOf(blockToBeMoved); 
                    assert(idxToBeMoved >= 0);
                    topLevelBlocks.splice(idxToBeMoved, 1);
                }
            }
        }
    };

    var onDragMove = function(x, y) {
        if (blockMoveState.state === EditorState.Dragging) {
            var pos = blockMoveState.block;
            pos.x = x - blockMoveState.x;
            pos.y = y - blockMoveState.y;
            dirty = true;
        }
        else if (blockMoveState.state === EditorState.MenuButtonClicked) {
            // If the pointer leaves the button area, we are choosing the option now...
            var m = blockMoveState.menu;
            var buttonX = blockMoveState.menu.offsetX + blockMoveState.block.x;
            var buttonY = blockMoveState.menu.offsetY + blockMoveState.block.y;
            if (x < buttonX || x >= buttonX + m.width || y < buttonY || y >= buttonY + m.height) {
                blockMoveState.state = EditorState.MenuChoosing;
                var option = pointToOption(x, y, blockMoveState.menu, blockMoveState.block.x, blockMoveState.block.y);
                if (option != -1 && blockMoveState.menu.currentOption !== option) {
                    blockMoveState.menu.currentOption = option;
                    dirty = true;
                }
            }
        }
        else if (blockMoveState.state === EditorState.MenuChoosing) {
            // If we are inside a menu option, highlight it (and redraw)
            var option = pointToOption(x, y, blockMoveState.menu, blockMoveState.block.x, blockMoveState.block.y);
            if (option != -1 && blockMoveState.menu.currentOption !== option) {
                blockMoveState.menu.currentOption = option;
                dirty = true;
            }
        }
    };

    var onDragStop = function(x, y) {
        if (blockMoveState.state === EditorState.Dragging) {
            // It was a block drag action
            var movedBlock = blockMoveState.block;

            if (movedBlock.x < factoryAreaWidth) {
                // The block overlaps the factory area -> discard it
                // (TODO - some animation) 
                blockMoveState.state = EditorState.None;
                if (!movedBlock.onDelete()) {
                    movedBlock.x = blockMoveState.xOriginal;
                    movedBlock.y = blockMoveState.yOriginal;
                    topLevelBlocks.push(movedBlock);
                }
                blockMoveState.block = null;
                dirty = true;
                return;
            }

            // TODO - comment
            var bestParentConn = null;
            for (var i = 0; i < topLevelBlocks.length; i++) {
                bestParentConn = bestDistanceConnector(movedBlock, topLevelBlocks[i], topLevelBlocks[i].x, topLevelBlocks[i].y, bestParentConn, true);
            }

            // TODO comment
            var bestChildConn = null;
            for (var i = 0; i < topLevelBlocks.length; i++) {
                bestChildConn = bestDistanceConnector(topLevelBlocks[i], movedBlock, movedBlock.x, movedBlock.y, bestChildConn, false);
            }

            // Discard any connections that are too far
            if (bestParentConn !== null && bestParentConn.d2 > distanceThreshold2) {
                bestParentConn = null;
            }
            if (bestChildConn !== null && bestChildConn.d2 > distanceThreshold2) {
                bestChildConn = null
            }

            if (bestParentConn !== null && (bestChildConn === null || bestParentConn.d2 < bestChildConn.d2)) {
                // We are connecting the dragged block's parent onto a static pin
                if (bestParentConn.outputSlot.block !== null) {
                    // If the pin was full, move the original child away 
                    var movedBlockDefaultOut = movedBlock;
                    while (movedBlockDefaultOut.childrenSlots[0].block !== null) {
                        movedBlockDefaultOut = movedBlockDefaultOut.childrenSlots[0].block;
                    }

                    var disconnectedBlock = bestParentConn.outputSlot.block;
                    // If the disconnected block can be reconnected (i.e., the dragged block 
                    // can be inserted between a parent and child), then do it!
                    if (areMatchingConnectors(movedBlockDefaultOut.childrenSlots[0], disconnectedBlock.parentSlot)) {
                        movedBlockDefaultOut.childrenSlots[0].block = disconnectedBlock;
                        disconnectedBlock.parentSlot.block = movedBlockDefaultOut;
                    }
                    else {
                        // Otherwise, move the disconnected child a bit away
                        disconnectedBlock.x = movedBlock.x + 40;
                        disconnectedBlock.y = movedBlock.y + 40;
                        addToTopLevelBlocks(topLevelBlocks, disconnectedBlock);
                        bestParentConn.outputSlot.block = null;
                    }
                }
                // Simply connect the dragged block as a child (and recalculate the positions to align)
                bestParentConn.outputSlot.block = movedBlock;
                movedBlock.parentSlot.block = bestParentConn.outputBlock;
                recalcBlockSizesList(topLevelBlocks);
            }
            else if (bestChildConn !== null) {
                // We are connecting some output pin of the dragged block into
                // a static hole
                // TODO - remove the guy with the static hole from top level and connect
                //        it to the to the outout pin of the dragged block,
                //        then add the dragged block to the top level
                //        recalculate the position of the dragged block + realign the rest   
                var idxInput = topLevelBlocks.indexOf(bestChildConn.inputBlock);
                assert(idxInput >= 0);
                bestChildConn.outputSlot.block = topLevelBlocks[idxInput];
                topLevelBlocks[idxInput].parentSlot.block = bestChildConn.outputBlock;
                topLevelBlocks[idxInput] = movedBlock;
            }
            else {
                // No connector was close enough -> just leave the block where it is and put it to 
                // the top level block list 
                topLevelBlocks.push(blockMoveState.block);
            }

            blockMoveState.state = EditorState.None;
            blockMoveState.block = null;
            dirty = true;
        }
        else if (blockMoveState.state === EditorState.MenuButtonClicked) {
            // We did not leave the menu button area, so the choice can be done with the next click
            // (There should be no need to redraw)
            blockMoveState.state = EditorState.MenuPreChoice;
                
        }
        else if (blockMoveState.state === EditorState.MenuPreChoice) {
            // Somehow we clicked in another window and released here -> discard the state
            blockMoveState.state = EditorState.None;
            dirty = true;
        }
        else if (blockMoveState.state === EditorState.MenuChoosing) {
            // We are finishing a menu choice
            // If we are inside a menu item, choose it.

            // Restore the default state
            blockMoveState.state = EditorState.None;
            dirty = true;
        }

        // TODO move around any overlaps?
    };

    // Prevent text selection
    $editCanvas.addEventListener('selectstart', function(e) { e.preventDefault(); return false; }, false);

    $editCanvas.addEventListener('mousedown', callMouseEvent(onDragStart));
    $editCanvas.addEventListener('mousemove', callMouseEvent(onDragMove));
    $editCanvas.addEventListener('mouseup', callMouseEvent(onDragStop));

    $editCanvas.addEventListener('touchstart', callTouchEvent(onDragStart));
    $editCanvas.addEventListener('touchmove', callTouchEvent(onDragMove));
    $editCanvas.addEventListener('touchend', callTouchEndEvent(onDragStop));

    setInterval(redraw, 30); 
}

